/*
 * Copyright (c) 2013 Hai Bison
 *
 * See the file LICENSE at the root directory of this project for copying
 * permission.
 */

package com.haibison.android.logs;

import static com.haibison.android.logs.providers.BaseCols._DATE_CREATED;
import static com.haibison.android.logs.providers.log.LogContract.COLUMN_LEVEL;
import static com.haibison.android.logs.providers.log.LogContract.COLUMN_MESSAGE;
import static com.haibison.android.logs.providers.log.LogContract.COLUMN_TAG;
import static com.haibison.android.logs.providers.log.LogContract.COLUMN_THROWABLE;
import static com.haibison.android.logs.providers.log.LogContract.LEVEL_ASSERT;
import static com.haibison.android.logs.providers.log.LogContract.LEVEL_DEBUG;
import static com.haibison.android.logs.providers.log.LogContract.LEVEL_ERROR;
import static com.haibison.android.logs.providers.log.LogContract.LEVEL_INFO;
import static com.haibison.android.logs.providers.log.LogContract.LEVEL_VERBOSE;
import static com.haibison.android.logs.providers.log.LogContract.LEVEL_WARN;
import static com.haibison.android.logs.providers.log.LogContract.TABLE_NAME;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;

import com.haibison.android.logs.providers.ProviderUtils;
import com.haibison.android.logs.providers.log.LogContract;
import com.haibison.android.logs.providers.log.LogProvider;

/**
 * Helper class for working with {@link LogProvider}.
 * <p/>
 * Maximum records for logs is {@code 999}. You can change it with
 * {@link Settings#setMaxLogRecords(Context, int)} to fit your needs. The
 * library automatically deletes overflow records when needed.
 * 
 * @author Hai Bison
 * @since v1.0 beta
 */
public class Al {

    /**
     * Map of log levels to their <i>abbreviation</i> names.
     */
    public static final SparseArray<Character> MAP_LOG_LEVELS = new SparseArray<Character>();

    private static final String UTF8 = "UTF-8";

    /**
     * Default log date format.
     * <p/>
     * Form: {@code "yyyy-MM-dd HH:mm:ss"}.
     */
    private static final DateFormat LOG_DATE_FORMAT = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");

    static {
        MAP_LOG_LEVELS.put(LEVEL_ASSERT, 'A');
        MAP_LOG_LEVELS.put(LEVEL_DEBUG, 'D');
        MAP_LOG_LEVELS.put(LEVEL_ERROR, 'E');
        MAP_LOG_LEVELS.put(LEVEL_INFO, 'I');
        MAP_LOG_LEVELS.put(LEVEL_VERBOSE, 'V');
        MAP_LOG_LEVELS.put(LEVEL_WARN, 'W');
    }// static

    /**
     * This is singleton class.
     */
    private Al() {
    }// Al()

    /**
     * Writes an assertion log.
     * 
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void a(Context context, Object tag, String msg,
            Object... params) {
        a(null, context, tag, msg, params);
    }// a()

    /**
     * Writes an assertion log.
     * 
     * @param t
     *            an exception to log.
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void a(Throwable t, Context context, Object tag, String msg,
            Object... params) {
        log(context, t, LEVEL_ASSERT, tag, msg, params);
    }// a()

    /**
     * Writes a debug log.
     * 
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void d(Context context, Object tag, String msg,
            Object... params) {
        d(null, context, tag, msg, params);
    }// d()

    /**
     * Writes a debug log.
     * 
     * @param t
     *            an exception to log.
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void d(Throwable t, Context context, Object tag, String msg,
            Object... params) {
        log(context, t, LEVEL_DEBUG, tag, msg, params);
    }// d()

    /**
     * Writes an error log.
     * 
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void e(Context context, Object tag, String msg,
            Object... params) {
        e(null, context, tag, msg, params);
    }// e()

    /**
     * Writes an error log.
     * 
     * @param t
     *            an exception to log.
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void e(Throwable t, Context context, Object tag, String msg,
            Object... params) {
        log(context, t, LEVEL_ERROR, tag, msg, params);
    }// e()

    /**
     * Writes an info log.
     * 
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void i(Context context, Object tag, String msg,
            Object... params) {
        i(null, context, tag, msg, params);
    }// i()

    /**
     * Writes an info log.
     * 
     * @param t
     *            an exception to log.
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void i(Throwable t, Context context, Object tag, String msg,
            Object... params) {
        log(context, t, LEVEL_INFO, tag, msg, params);
    }// i()

    /**
     * Writes a verbose log.
     * 
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void v(Context context, Object tag, String msg,
            Object... params) {
        v(null, context, tag, msg, params);
    }// v()

    /**
     * Writes a verbose log.
     * 
     * @param t
     *            an exception to log.
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void v(Throwable t, Context context, Object tag, String msg,
            Object... params) {
        log(context, t, LEVEL_VERBOSE, tag, msg, params);
    }// v()

    /**
     * Writes a warning log.
     * 
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void w(Context context, Object tag, String msg,
            Object... params) {
        w(null, context, tag, msg, params);
    }// w()

    /**
     * Writes a warning log.
     * 
     * @param t
     *            an exception to log.
     * @param context
     *            the context.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    public static void w(Throwable t, Context context, Object tag, String msg,
            Object... params) {
        log(context, t, LEVEL_WARN, tag, msg, params);
    }// w()

    /**
     * Writes a log.
     * 
     * @param context
     *            the context.
     * @param t
     *            an exception to log, can be {@code null}.
     * @param level
     *            log level, can be one of {@code LogContract.LEVEL_*}.
     * @param tag
     *            the tag.
     * @param msg
     *            the message.
     * @param params
     *            the parameters which will be formatted with {@code msg}.
     */
    private static void log(Context context, Throwable t, int level,
            Object tag, String msg, Object... params) {
        if (BuildConfig.DEBUG) {
            switch (level) {
            case LEVEL_ASSERT:
                System.out.printf("[%s] %s\n", tag, String.format(msg, params));
                if (t != null)
                    System.out.println(t);
                break;
            case LEVEL_DEBUG:
                if (t == null)
                    Log.d(String.valueOf(tag), String.format(msg, params));
                else
                    Log.d(String.valueOf(tag), String.format(msg, params), t);
                break;
            case LEVEL_ERROR:
                if (t == null)
                    Log.e(String.valueOf(tag), String.format(msg, params));
                else
                    Log.e(String.valueOf(tag), String.format(msg, params), t);
                break;
            case LEVEL_INFO:
                if (t == null)
                    Log.i(String.valueOf(tag), String.format(msg, params));
                else
                    Log.i(String.valueOf(tag), String.format(msg, params), t);
                break;
            case LEVEL_VERBOSE:
                if (t == null)
                    Log.v(String.valueOf(tag), String.format(msg, params));
                else
                    Log.v(String.valueOf(tag), String.format(msg, params), t);
                break;
            case LEVEL_WARN:
                if (t == null)
                    Log.w(String.valueOf(tag), String.format(msg, params));
                else
                    Log.w(String.valueOf(tag), String.format(msg, params), t);
                break;
            }
        }// if DEBUG

        ContentValues values = new ContentValues();
        values.put(COLUMN_LEVEL, level);
        values.put(COLUMN_TAG, String.valueOf(tag));
        values.put(COLUMN_MESSAGE, String.format(msg, params));
        if (t != null)
            values.put(COLUMN_THROWABLE, printStackTrace(t).toString());

        if (context.getContentResolver().insert(
                LogContract.genContentUri(context), values) != null) {
            /*
             * Check max lines...
             */
            if (ProviderUtils.getRecordCount(context,
                    LogContract.genContentUri(context)) > Settings
                    .getMaxLogRecords(context)) {
                ProviderUtils.deleteOldestRecord(context,
                        LogContract.genContentUri(context), TABLE_NAME);
            }
        }// if
    }// log()

    /**
     * Prints the stack trace of a {@link Throwable}.
     * 
     * @param t
     *            an exception.
     * @return the stack trace.
     */
    public static CharSequence printStackTrace(Throwable t) {
        return printStackTrace(t, new StringBuilder());
    }// printStackTrace()

    /**
     * Prints the stack trace of a {@link Throwable}.
     * 
     * @param t
     *            an exception.
     * @param s
     *            the target string.
     * @return the input parameter {@code s} containing stack trace. This also
     *         means you can cast the result back to {@link StringBuilder}.
     */
    public static CharSequence printStackTrace(Throwable t, StringBuilder s) {
        StringWriter stringWriter = new StringWriter();
        PrintWriter printWriter = new PrintWriter(stringWriter);
        try {
            t.printStackTrace(printWriter);
            printWriter.flush();
            s.append(stringWriter.toString());
            return s;
        } finally {
            printWriter.close();
        }
    }// printStackTrace()

    /**
     * Prints the whole logs.
     * 
     * @param context
     *            the context.
     * @param s
     *            the target string.
     * @return the input parameter {@code s} containing logs. This also means
     *         you can cast the result back to {@link StringBuilder}.
     */
    public static CharSequence printLogs(Context context, StringBuilder s) {
        Cursor cursor = context.getContentResolver().query(
                LogContract.genContentUri(context), null, null, null, null);
        if (cursor == null)
            return s;

        try {
            int colDateCreated = -1, colLevel = -1, colTag = -1, colMsg = -1, colThrowable = -1;
            while (cursor.moveToNext()) {
                /*
                 * Column indexes.
                 */
                if (colDateCreated < 0)
                    colDateCreated = cursor.getColumnIndex(_DATE_CREATED);
                if (colLevel < 0)
                    colLevel = cursor.getColumnIndex(COLUMN_LEVEL);
                if (colMsg < 0)
                    colMsg = cursor.getColumnIndex(COLUMN_MESSAGE);
                if (colTag < 0)
                    colTag = cursor.getColumnIndex(COLUMN_TAG);
                if (colThrowable < 0)
                    colThrowable = cursor.getColumnIndex(COLUMN_THROWABLE);

                /*
                 * The log.
                 */
                s.append(MAP_LOG_LEVELS.get(cursor.getInt(colLevel))).append(
                        '/');
                s.append(
                        LOG_DATE_FORMAT.format(new Date(Long.parseLong(cursor
                                .getString(colDateCreated))))).append('/');
                s.append(cursor.getString(colTag)).append(": ");
                s.append(cursor.getString(colMsg)).append('\n');

                String msg = cursor.getString(colThrowable);
                if (!TextUtils.isEmpty(msg))
                    s.append(msg).append('\n');
            }// while
        } finally {
            cursor.close();
        }

        return s;
    }// printLogs()

    /**
     * Prints the whole logs.
     * 
     * @param context
     *            the context.
     * @return the whole log.
     */
    public static CharSequence printLogs(Context context) {
        return printLogs(context, new StringBuilder());
    }// printLogs()

    /**
     * Prints the whole logs.
     * 
     * @param context
     *            the context.
     * @param outputStream
     *            the output stream.
     * @throws IOException
     *             if any.
     */
    public static void printLogs(Context context, OutputStream outputStream)
            throws IOException {
        Cursor cursor = context.getContentResolver().query(
                LogContract.genContentUri(context), null, null, null, null);
        if (cursor == null)
            return;

        try {
            int colDateCreated = -1, colLevel = -1, colTag = -1, colMsg = -1, colThrowable = -1;
            while (cursor.moveToNext()) {
                /*
                 * Column indexes.
                 */
                if (colDateCreated < 0)
                    colDateCreated = cursor.getColumnIndex(_DATE_CREATED);
                if (colLevel < 0)
                    colLevel = cursor.getColumnIndex(COLUMN_LEVEL);
                if (colMsg < 0)
                    colMsg = cursor.getColumnIndex(COLUMN_MESSAGE);
                if (colTag < 0)
                    colTag = cursor.getColumnIndex(COLUMN_TAG);
                if (colThrowable < 0)
                    colThrowable = cursor.getColumnIndex(COLUMN_THROWABLE);

                /*
                 * Level.
                 */
                outputStream.write(String.format(
                        "%s/%s/%s: %s\n",
                        MAP_LOG_LEVELS.get(cursor.getInt(colLevel)),
                        LOG_DATE_FORMAT.format(new Date(Long.parseLong(cursor
                                .getString(colDateCreated)))),
                        cursor.getString(colTag), cursor.getString(colMsg))
                        .getBytes(UTF8));

                String msg = cursor.getString(colThrowable);
                if (!TextUtils.isEmpty(msg))
                    outputStream.write((msg + '\n').getBytes(UTF8));
            }// while
        } finally {
            cursor.close();
        }
    }// printLogs()

    /**
     * Prints the whole logs.
     * 
     * @param context
     *            the context.
     * @param writer
     *            the writer.
     * @throws IOException
     *             if any.
     */
    public static void printLogs(Context context, Writer writer)
            throws IOException {
        Cursor cursor = context.getContentResolver().query(
                LogContract.genContentUri(context), null, null, null, null);
        if (cursor == null)
            return;

        try {
            int colDateCreated = -1, colLevel = -1, colTag = -1, colMsg = -1, colThrowable = -1;
            while (cursor.moveToNext()) {
                /*
                 * Column indexes.
                 */
                if (colDateCreated < 0)
                    colDateCreated = cursor.getColumnIndex(_DATE_CREATED);
                if (colLevel < 0)
                    colLevel = cursor.getColumnIndex(COLUMN_LEVEL);
                if (colMsg < 0)
                    colMsg = cursor.getColumnIndex(COLUMN_MESSAGE);
                if (colTag < 0)
                    colTag = cursor.getColumnIndex(COLUMN_TAG);
                if (colThrowable < 0)
                    colThrowable = cursor.getColumnIndex(COLUMN_THROWABLE);

                /*
                 * Level.
                 */
                writer.write(String.format("%s/%s/%s: %s\n", MAP_LOG_LEVELS
                        .get(cursor.getInt(colLevel)), LOG_DATE_FORMAT
                        .format(new Date(Long.parseLong(cursor
                                .getString(colDateCreated)))), cursor
                        .getString(colTag), cursor.getString(colMsg)));

                String msg = cursor.getString(colThrowable);
                if (!TextUtils.isEmpty(msg))
                    writer.write(msg + '\n');
            }// while
        } finally {
            cursor.close();
        }
    }// printLogs()

    /**
     * Clears the whole logs.
     * 
     * @param context
     *            the context.
     * @return The number of rows (log records) deleted.
     */
    public static int clearLogs(Context context) {
        return context.getContentResolver().delete(
                LogContract.genContentUri(context), null, null);
    }// clearLogs()

}
